import { task } from 'gulp';
import { walkDirAndFilter, getBaseNameWithoutExt } from '../../util';
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
import { srcPath, autoPath } from '../../config';

const MODEL_FILE_TYPES = ['class', 'interface', 'type', 'const']

type ModelType = typeof MODEL_FILE_TYPES[number];

class Model {
  name: string;
  type: ModelType;
  content: string;

  constructor(name: string, type: string, content: string) {
    this.name = name;
    this.type = type;
    this.content = content;
  }
}

class ModelComponentMap {
  [key: string]: Model[];
}

task('genModel', (done) => {
  const modelMap: ModelComponentMap = {};
  const filePaths: string[] = [];

  // walk through all files
  walkDirAndFilter(path.join(srcPath, './fui'), '.md', (dir: any) => {
    const componentName = getBaseNameWithoutExt(dir.path);
    const dirName = path.dirname(dir.path);
    const modelArray: Model[] = [];
    walkDirAndFilter(dirName, '.ts', (file: any) => {
      const filePath = file.path;
      const fileName = path.basename(filePath);

      filePaths.push(filePath);
      const fileModelArray = parseSourceFile(fileName, filePath);
      modelArray.push(...fileModelArray);
    });

    modelMap[componentName] = modelArray.sort((modelA, modelB) => {
      const modelAIndex = MODEL_FILE_TYPES.indexOf(modelA.type);
      const modelBIndex = MODEL_FILE_TYPES.indexOf(modelB.type);
      return modelAIndex - modelBIndex;
    });
  });

  fs.writeFileSync(
    path.join(autoPath, 'model.ts'), `export default ${JSON.stringify(modelMap, null, 2)}`,
  ),
  done();
});

function parseSourceFile(fileName: string, filePath: string) {
  // Skip test files
  if (fileName.indexOf('spec') > -1 || fileName.indexOf('control') > -1) {
    return [];
  }

  const modelArray: Model[] = [];
  const sourceFile = ts.createSourceFile(
    fileName,
    fs.readFileSync(filePath, 'utf8'),
    ts.ScriptTarget.Latest,
  );
  // Find each class declarations with decorator
  sourceFile.forEachChild(node => {

    if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) ||
      ts.isTypeAliasDeclaration(node)) {
      const model = parseDeclaration(node, sourceFile);
      if (model) {
        modelArray.push(model);
      }
    }

    if (ts.isVariableStatement(node)) {
      if (node.declarationList) {
        node.declarationList.forEachChild((declaration) => {
          if (ts.isVariableDeclaration(declaration)) {
            const model = parseVariable(node, declaration, sourceFile);
            if (model) {
              modelArray.push(model);
            }
          }
        });
      }
    }
  });

  return modelArray;
}

function parseDeclaration(node: ts.NamedDeclaration, sourceFile: ts.SourceFile): Model | undefined {
  if (node.decorators) {
    return;
  }

  const combinedFlag = ts.getCombinedModifierFlags(node);
  if ((combinedFlag & ts.ModifierFlags.Export) !== ts.ModifierFlags.Export) {
    return;
  }

  const nodeName = (node.name as ts.Identifier).escapedText as string;
  const nodeType = parseTypeName(node);

  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
  const nodeContent = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

  return new Model(
    nodeName,
    nodeType,
    nodeContent,
  );
}

function parseVariable(statement: ts.Statement, declaration: ts.VariableDeclaration, sourceFile: ts.SourceFile): Model | undefined {
  const modifiers = statement.modifiers;
  if (!modifiers) {
    return;
  }
  if (!modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
    return;
  }

  const nodeName = (declaration.name as ts.Identifier).escapedText as string;
  const nodeType = parseTypeName(declaration);

  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
  const nodeContent = printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile);

  return new Model(
    nodeName,
    nodeType,
    nodeContent,
  );
}

function parseTypeName(node: ts.Node): ModelType {
  if (ts.isClassDeclaration(node)) {
    return 'class';
  } else if (ts.isInterfaceDeclaration(node)) {
    return 'interface';
  } else if (ts.isTypeAliasDeclaration(node)) {
    return 'type';
  } else if (ts.isVariableDeclaration(node)) {
    return 'const';
  }
  return '';
}
